import os
import socket
import exceptions

from support import Singleton
from support.path import path
from _connections import create_connection, NonzeroReturnStatus
import _io

_FULLNAME_FOR_LOCAL_USER_ON_LOCAL_MACHINE = "$"


class UserConfigurationMismatch(exceptions.Exception): pass
class UserConnectionAlreadyChosen(exceptions.Exception): pass
class StrategyDeterminationError(exceptions.Exception): pass


def _get_ip_from_hostname(host):
    return socket.getaddrinfo(host, None)[0][4][0]


class UserManager(Singleton):
    
    use_dry_run_strategies = property(lambda self: self.__use_dry_run_strategies)

    def __init__(self):
        super(UserManager, self).__init__()
        self.user_datas = {}
        # FIXME: Issue 21: encrypt these using a temporal key like md5(systime + random)?
        self.passwords_by_group = {}
        self.__use_dry_run_strategies = False
    
    def get_user_data(self, fullname):
        if fullname not in self.user_datas:
            self.user_datas[fullname] = _UserData(fullname)
        return self.user_datas[fullname]
    
    def get_password_for_group(self, group):
        if group not in self.passwords_by_group:
            users_in_group = self.find_users_in_password_group(group)
            fullnames = [user.fullname for user in users_in_group]
            prompt = "%s\nPassword: " % "\n".join(fullnames)
            self.passwords_by_group[group] = _io.get_password_input(prompt)
        return self.passwords_by_group[group]
    
    def find_users_in_password_group(self, password_group):
        user_fullnames = []
        for fullname in self.user_datas:
            user_data = self.user_datas[fullname]
            if user_data.password_group == password_group:
                user_fullnames.append(user_data.fullname)
        user_fullnames = list(set(user_fullnames))
        return [User(fullname) for fullname in user_fullnames]
    
    def force_all_users_to_use_dry_run_strategies(self):
        self.__use_dry_run_strategies = True

    def do_not_use_dry_run_strategies(self):
        self.__use_dry_run_strategies = True


class _UserData(object):
    
    def _get_ip(self):
        if not self.__ip:
            if self.__host != "localhost":
                self.__ip = _get_ip_from_hostname(self.host)
            else:
                self.__ip = None
        return self.__ip
        
    def _get_detail(self):
        if not self.__detail:
            if self.__host != "localhost":
                self.__detail = self.ip
            else:
                self.__detail = "locally on %s" % socket.gethostname()
        return self.__detail
    
    def _get_connection(self):
        # lazy-load connection
        if not self.__connection:
            
            if self.su_from:
                # we are su'ing from another user
                self.__connection = create_connection(
                    username = self.name,
                    hostname = self.host,
                    port = self.port,
                    getpass = lambda: self.password,
                    su_source_username = self.su_from.name,
                    su_source_getpass = lambda: self.su_from.password,
                )
            
            else:
                # we have a normal connection
                self.__connection = create_connection(
                    username = self.name,
                    hostname = self.host,
                    port = self.port,
                    getpass = lambda: self.password
                )
        
        # return the connection
        return self.__connection

    def _get_password(self):
        return UserManager.get_instance().get_password_for_group(self.password_group)
    
    def _set_su_from(self, username_or_user):
        if self.__connection:
            raise UserConnectionAlreadyChosen("the connection for this user has already been established, configuration changes not allowed")
        if isinstance(username_or_user, User):
            potential_su_user = username_or_user
        else:
            potential_su_user = User(username_or_user)
        if self.__su_from and potential_su_user != self.__su_from:
            raise UserConfigurationMismatch("cannot change su_from once it has been set")
        if potential_su_user.host == self.host:
            self.__su_from = potential_su_user
        else:
            raise UserConfigurationMismatch("cannot su_from a user on a different host (%s cannot su_from %s)" % (self.fullname, potential_su_user.fullname))
    
    def _set_port(self, port):
        if self.__connection:
            raise UserConnectionAlreadyChosen("the connection for this user has already been established, configuration changes not allowed")
        if self.__port and port != self.__port:
            raise UserConfigurationMismatch("cannot change port once it has been set")
        self.__port = port
    
    def _set_password_group(self, password_group):
        if self.__connection:
            raise UserConnectionAlreadyChosen("the connection for this user has already been established, configuration changes not allowed")
        if self.__password_group and password_group != self.__password_group:
            raise UserConfigurationMismatch("cannot change password_group once it has been set")
        self.__password_group = str(password_group)
    
    # read-only properties
    name = property(lambda self: self.__name)
    host = property(lambda self: self.__host)
    fullname = property(lambda self: self.__fullname)
    
    # lazy-loaded properties
    ip = property(_get_ip)
    detail = property(_get_detail)
    password = property(_get_password)
    connection = property(_get_connection)
    
    # read-write properties
    port = property(lambda self: self.__port, _set_port) # TODO: allow port to be configured
    su_from = property(lambda self: self.__su_from, _set_su_from)
    password_group = property(lambda self: self.__password_group, _set_password_group)
    
    def __init__(self, fullname):
                
        if fullname == _FULLNAME_FOR_LOCAL_USER_ON_LOCAL_MACHINE:
            self.__name = os.getlogin()
            self.__host = "localhost"
            self.__fullname = "%s@localhost" % self.__name
        else:
            pieces = fullname.split("@")
            if len(pieces) != 2:
                raise ValueError("'%s' is not a valid, fully-qualified username (e.g. user@example.com)")
            self.__name = str(pieces[0])
            self.__host = str(pieces[1])
            self.__fullname = fullname
            
        if self.__host == "localhost":
            self.__password_group = "autogroup:localhost"
        else:
            self.__password_group = None
        self.__su_from = None
        
        # lazy-loaded properties
        self.__ip = None
        self.__detail = None
        self.__connection = None
        self.__port = None
        

class User(object):
    
    name = property(lambda self: self.__data.name)
    host = property(lambda self: self.__data.host)
    port = property(lambda self: self.__data.port)
    ip = property(lambda self: self.__data.ip)
    detail = property(lambda self: self.__data.detail)
    fullname = property(lambda self: self.__data.fullname)
    su_from = property(lambda self: self.__data.su_from)
    password_group = property(lambda self: self.__data.password_group)
    password = property(lambda self: self.__data.password)
    
    def __init__(self, fullname):
        self.__data = UserManager.get_instance().get_user_data(fullname)
    
    def __eq__(self,other):
        if isinstance(other,User):
            return self.fullname == other.fullname
        return False
    
    def __ne__(self,other):
        return not self.__eq__(other)
    
    def __hash__(self):
        return hash(self.__data.fullname)
    
    def __str__(self):
        return self.fullname
    
    def configure(self, password_group = None, su_from = None, port = None):
        if password_group:
            self.__data.password_group = password_group
        if su_from:
            self.__data.su_from = su_from
        if port:
            self.__data.port = port
        
        # return self for chaining ability
        return self
    
    def create_command_stream(self, cmd):
        return self.__data.connection.create_command_stream(cmd)
    
    def create_generator_stream(self, generator, virtualenv = None):
        return self.__data.connection.create_generator_stream(generator, virtualenv)
    
    def pull(self, source, destination):

        # get the source as a path object
        source_path = None
        if isinstance(source, path):
            source_path = source
        elif isinstance(source, basestring):
            source_path = path(source)
        
        # get the destination as a path object
        destination_path = None
        if isinstance(destination, path):
            destination_path = destination
        elif isinstance(destination, basestring):
            destination_path = path(source)
        
        # determine if the remote filename is actually a directory to place into
        path_separator = os.path.normcase('/')
        # if destination_path.endswith( path_separator ):
        #     destination_path = remote_filename + os.path.basename(local_filename)
        # 
        
        # get a queue of remote operations to perform
        remote_dirs_to_create = []
        files_to_send = {}
        
        if os.path.isdir( source_path ):
            
            def directory_visitor(arg, local_currdir, filenames):
                # parse the dir_mapping arg
                local_dir, remote_dir = arg
                
                # figure out the mapped directory
                remote_currdir = os.path.normpath( local_currdir.replace(local_dir, remote_dir) )
                remote_dirs_to_create.append( remote_currdir )
                
                for filename in filenames:
                    local_filename = os.path.join(local_currdir, filename)
                    if os.path.isfile(local_filename):
                        remote_filename = os.path.normpath(local_filename.replace(local_dir, remote_dir))
                        files_to_send[local_filename] = remote_filename
            
            # walk the directory structure
            os.path.walk(source_path, directory_visitor, (source_path, destination_path))
            
        else:
            
            files_to_send[local_filename] = remote_filename
        
        # sort the remote directory creation queue
        properly_ordered_remote_dirs_to_create = sorted(remote_dirs_to_create, lambda x,y: len(x) > len(y))
        
        # successfully pulled all send specifications, execute them all at once
        # TODO: parallelize this?
        sftp_client = self.ssh_client.open_sftp()
        
        for new_dir in properly_ordered_remote_dirs_to_create:
            self._mkdir(sftp_client, new_dir)
        
        for local_filename in files_to_send:
            remote_filename = files_to_send[local_filename]
            self._put(sftp_client, local_filename, remote_filename)
        
    def expect_password_for(self, pattern):
        """adds a password expectation that the user must respond to"""
        raise NotImplementedError("this is a placeholder during refactoring")

    def expect_input_for(self, pattern):
        """adds an input expectation that the user must respond to"""
        raise NotImplementedError("this is a placeholder during refactoring")

    def respond_with(self, pattern, response):
        """adds an expectation that is automatically responded to"""
        raise NotImplementedError("this is a placeholder during refactoring")
        
    def respond_with_password(self, pattern, response):
        """adds a password expecation that is automatically responded to"""
        raise NotImplementedError("this is a placeholder during refactoring")
    
    def stop_expecting(self, pattern):
        """tells the strategy to stop expecting input for a
        particular pattern (password or input)"""
        raise NotImplementedError("this is a placeholder during refactoring")
    
    def stop_responding_to(self, pattern):
        """tells the strategy to stop automatically responding for
        a particular pattern (password or input)"""
        raise NotImplementedError("this is a placeholder during refactoring")
            

